using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using Org.Mentalis.Network.ProxySocket;

namespace ProxyServer.ProxyImpl
{
    public sealed class HttpClient : Client
    {
        private readonly ILogger logger = Logger.GetLogger();

        private readonly StringBuilder queryBuilder = new StringBuilder();

        private HttpRequestContext context;

        public HttpClient(Socket clientSocket, DestroyDelegate destroyer)
            : base(clientSocket, destroyer)
        {
        }

        /// <summary>
        /// if we do not change a connection, then a new relay is not need
        /// </summary>
        private bool NeedRelay { get; set; }

        public void StartRelay()
        {
            try
            {
                var pipedSocket = new PipedSocket(ClientSocket, DestinationSocket);
                pipedSocket.StartRelay();
            }
            catch
            {
                Dispose();
            }
        }

        private void ProcessQuery()
        {
            try
            {
                NeedRelay = false;
                var destinationEndPoint = new IPEndPoint(Dns.GetHostEntry(context.Host).AddressList[0], context.Port);
                if (DestinationSocket != null)
                {
                    if (destinationEndPoint.ToString() != DestinationSocket.TargetEndpoint.ToString())
                    {
                        DestinationSocket.Shutdown(SocketShutdown.Both);
                        DestinationSocket.Close();
                        ConnectToTarget(destinationEndPoint);
                    }
                    else
                    {
                        SendRequest();
                    }
                }
                else
                {
                    ConnectToTarget(destinationEndPoint);
                }
            }
            catch
            {
                SendBadRequest();
                return;
            }
        }

        private void ConnectToTarget(EndPoint destinationEndPoint)
        {
            var proxySetting = ProxyConfigManager.ReadSetting();
            DestinationSocket = new ProxySocket(destinationEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
                                    {
                                        ProxyEndPoint = new IPEndPoint(IPAddress.Parse(proxySetting.ProxyHost), proxySetting.ProxyPort),
                                        ProxyType = ProxyTypes.Socks5,
                                        ProxyUser = proxySetting.ProxyUser,
                                        ProxyPass = proxySetting.ProxyPass
                                    };
            if ("keep-alive".Equals(context["Proxy-Connection"], StringComparison.CurrentCultureIgnoreCase))
            {
                DestinationSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, 1);
            }
            DestinationSocket.BeginConnect(destinationEndPoint, OnConnected, DestinationSocket);
            NeedRelay = true;
        }

        public override void StartHandshake()
        {
            try
            {
                ClientSocket.BeginReceive(Buffer.Buffer, Buffer.Offset, Buffer.Size, SocketFlags.None, new AsyncCallback(OnReceiveQuery), ClientSocket);
            }
            catch
            {
                Dispose();
            }
        }

        private bool IsValidQuery(string query)
        {
            int index = query.IndexOf("\r\n\r\n");
            if (index == -1)
                return false;

            context = HttpRequestContext.ParseQuery(query);

            if (string.IsNullOrEmpty(context["Host"]))
            {
                SendBadRequest();
                return false;
            }

            if ("POST".Equals(context.HttpRequestType, StringComparison.CurrentCultureIgnoreCase))
            {
                try
                {
                    int length = int.Parse(context["Content-Length"]);
                    return query.Length >= index + 6 + length;
                }
                catch
                {
                    SendBadRequest();
                    return true;
                }
            }

            return true;
        }

        private void SendBadRequest()
        {
            const string brs =
                "HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Type: text/html\r\n\r\n<html><head><title>400 Bad Request</title></head><body><div align=\"center\"><table border=\"0\" cellspacing=\"3\" cellpadding=\"3\" bgcolor=\"#C0C0C0\"><tr><td><table border=\"0\" width=\"500\" cellspacing=\"3\" cellpadding=\"3\"><tr><td bgcolor=\"#B2B2B2\"><p align=\"center\"><strong><font size=\"2\" face=\"Verdana\">400 Bad Request</font></strong></p></td></tr><tr><td bgcolor=\"#D1D1D1\"><font size=\"2\" face=\"Verdana\"> The proxy server could not understand the HTTP request!<br><br> Please contact your network administrator about this problem.</font></td></tr></table></center></td></tr></table></div></body></html>";
            try
            {
                ClientSocket.BeginSend(Encoding.ASCII.GetBytes(brs), 0, brs.Length, SocketFlags.None, new AsyncCallback(OnErrorSent), ClientSocket);
            }
            catch
            {
                Dispose();
            }
        }

        public override string ToString()
        {
            return ToString(false);
        }

        public string ToString(bool withUrl)
        {
            string ret;
            try
            {
                if (DestinationSocket == null || DestinationSocket.RemoteEndPoint == null)
                    ret = "Incoming HTTP connection from " + ((IPEndPoint)ClientSocket.RemoteEndPoint).Address;
                else
                    ret = "HTTP connection from " + ((IPEndPoint)ClientSocket.RemoteEndPoint).Address + " to " + ((IPEndPoint)DestinationSocket.RemoteEndPoint).Address + " on port " + ((IPEndPoint)DestinationSocket.RemoteEndPoint).Port;

                if (context != null && !string.IsNullOrEmpty(context["Host"]))
                    ret += "\r\n" + " requested URL: http://" + context["Host"] + context.RequestedPath;
            }
            catch
            {
                ret = "HTTP Connection";
            }
            return ret;
        }

        private void OnReceiveQuery(IAsyncResult ar)
        {
            int ret;
            try
            {
                ret = ClientSocket.EndReceive(ar);
            }
            catch
            {
                ret = -1;
            }

            if (ret <= 0)
            {
                Dispose();
                return;
            }

            queryBuilder.Append(Encoding.ASCII.GetString(Buffer.Buffer, Buffer.Offset, ret));

            if (IsValidQuery(queryBuilder.ToString()))
            {
                ProcessQuery();

                queryBuilder.Length = 0;
            }

            try
            {
                ClientSocket.BeginReceive(Buffer.Buffer, Buffer.Offset, Buffer.Size, SocketFlags.None, new AsyncCallback(OnReceiveQuery), ClientSocket);
            }
            catch
            {
                Dispose();
            }
        }

        private void OnErrorSent(IAsyncResult ar)
        {
            try
            {
                ClientSocket.EndSend(ar);
            }
            catch (Exception e)
            {
                logger.Error(e.Message);
                Dispose();
            }
        }

        private void OnConnected(IAsyncResult ar)
        {
            try
            {
                DestinationSocket.EndConnect(ar);
                SendRequest();
            }
            catch
            {
                Dispose();
            }
        }

        private void SendRequest()
        {
            logger.Info(ToString(true));
            string rq;
            if ("CONNECT".Equals(context.HttpRequestType, StringComparison.CurrentCultureIgnoreCase))
            {
                //HTTPS
                rq = context.HttpVersion + " 200 Connection established\r\nProxy-Agent: Mentalis Proxy Server\r\n\r\n";
                ClientSocket.BeginSend(Encoding.ASCII.GetBytes(rq), 0, rq.Length, SocketFlags.None, new AsyncCallback(OnOkSent), ClientSocket);
            }
            else
            {
                //Normal HTTP
                rq = context.RebuildQuery();
                DestinationSocket.BeginSend(Encoding.ASCII.GetBytes(rq), 0, rq.Length, SocketFlags.None, new AsyncCallback(OnQuerySent), DestinationSocket);
            }
        }

        private void OnQuerySent(IAsyncResult ar)
        {
            try
            {
                if (DestinationSocket.EndSend(ar) == -1)
                {
                    Dispose();
                    return;
                }
                if (NeedRelay)
                {
                    StartRelay();
                }
            }
            catch
            {
                Dispose();
            }
        }

        private void OnOkSent(IAsyncResult ar)
        {
            try
            {
                if (ClientSocket.EndSend(ar) == -1)
                {
                    Dispose();
                    return;
                }
                if (NeedRelay)
                {
                    StartRelay();
                }
            }
            catch
            {
                Dispose();
            }
        }
    }
}